package common.Util;

import common.MysqlCanalC;
import org.apache.commons.lang.ArrayUtils;

import java.io.ByteArrayOutputStream;
import java.io.IOException;

/**
 * @ClassName ByteUtil
 * @Description TODO
 * @Author fengk
 * @Date 2018/8/27 00:00
 */

public class ByteUtil {
    public static final long NULL_LENGTH = -1;
    public static final long ZERO_LENGTH=0;

    /**
     * index 开始读到0x00
     *
     * @param data
     * @param index
     * @return
     */
    public static byte[] readNullTerminatedBytes(byte[] data, int index) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        for (int i = index; i < data.length; i++) {
            byte item = data[i];
            if (item == MysqlCanalC.NULL_TERMINATED_STRING_DELIMITER) {
                break;
            }
            out.write(item);
        }
        return out.toByteArray();
    }

    public static byte[] readEOFTerminatedBytes(byte[] data, int index) {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        for (int i = index; i < data.length; i++) {
            byte item = data[i];
            out.write(item);
        }
        return out.toByteArray();
    }

    public static void writeNullTerminatedString(String str, ByteArrayOutputStream out) throws IOException {
        out.write(str.getBytes());
        out.write(MysqlCanalC.NULL_TERMINATED_STRING_DELIMITER);
    }

    public static void writeNullTerminated(byte[] data, ByteArrayOutputStream out) throws IOException {
        out.write(data);
        out.write(MysqlCanalC.NULL_TERMINATED_STRING_DELIMITER);
    }

    public static byte[] readFixedLengthBytes(byte[] data, int index, int length) {
        byte[] bytes = new byte[length];
        System.arraycopy(data, index, bytes, 0, length);
        return bytes;
    }

    public static long readUnsignedIntLittleEndian(byte[] data, int index) {
        long result = (long) (data[index] & 0xFF) | (long) ((data[index + 1] & 0xFF) << 8)
                | (long) ((data[index + 2] & 0xFF) << 16) | (long) ((data[index + 3] & 0xFF) << 24);
        return result;
    }

    public static long readUnsignedLongLittleEndian(byte[] data, int index) {
        long accumulation = 0;
        int position = index;
        for (int shiftBy = 0; shiftBy < 64; shiftBy += 8) {
            accumulation |= (long) ((data[position++] & 0xff) << shiftBy);
        }
        return accumulation;
    }

    public static int readUnsignedShortLittleEndian(byte[] data, int index) {
        int result = (data[index] & 0xFF) | ((data[index + 1] & 0xFF) << 8);
        return result;
    }

    public static int readUnsignedMediumLittleEndian(byte[] data, int index) {
        int result = (data[index] & 0xFF) | ((data[index + 1] & 0xFF) << 8) | ((data[index + 2] & 0xFF) << 16);
        return result;
    }

    /**
     * To convert a length-encoded integer into its numeric value, check the first byte:
     * <p>
     * If it is < 0xfb, treat it as a 1-byte integer.
     * <p>
     * If it is 0xfc, it is followed by a 2-byte integer.
     * <p>
     * If it is 0xfd, it is followed by a 3-byte integer.
     * <p>
     * If it is 0xfe, it is followed by a 8-byte integer.
     *
     * @param data
     * @param index
     * @return
     * @throws IOException
     */
    public static long readLengthCodedBinary(byte[] data, int index) throws IOException {
        int firstByte = data[index] & 0xFF;
        switch (firstByte) {
            case 251:
                return NULL_LENGTH;
            case 252:
                return readUnsignedShortLittleEndian(data, index + 1);
            case 253:
                return readUnsignedMediumLittleEndian(data, index + 1);
            case 254:
                return readUnsignedLongLittleEndian(data, index + 1);
            default:
                return firstByte;
        }
    }

    public static int readLengthCodedBinaryLen(byte[] data, int index) throws IOException {
        int firstByte = data[index] & 0xFF;
        switch (firstByte) {
            case 251:
                //不可能
                return 1;
            case 252:
                return 3;
            case 253:
                return 4;
            case 254:
                return 9;
            default:
                return 1;
        }
    }

    /**
     * To convert a length-encoded integer into its numeric value, check the first byte:
     * <p>
     * If it is < 0xfb, treat it as a 1-byte integer.
     * <p>
     * If it is 0xfc, it is followed by a 2-byte integer.
     * <p>
     * If it is 0xfd, it is followed by a 3-byte integer.
     * <p>
     * If it is 0xfe, it is followed by a 8-byte integer.
     *
     * @param data
     * @param index
     * @return
     * @throws IOException
     */

    public static byte[] readBinaryCodedLengthBytes(byte[] data, int index) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        out.write(data[index]);

        byte[] buffer = null;
        int value = data[index] & 0xFF;
        if (value == 251) {
            buffer = new byte[0];
        }
        if (value == 252) {
            buffer = new byte[2];
        }
        if (value == 253) {
            buffer = new byte[3];
        }
        if (value == 254) {
            buffer = new byte[8];
        }
        if (buffer != null) {
            try {
                System.arraycopy(data, index + 1, buffer, 0, buffer.length);
                out.write(buffer);
            }catch (ArrayIndexOutOfBoundsException e){
                e.printStackTrace();
            }

        }

        return out.toByteArray();
    }

    public static void write8ByteUnsignedIntLittleEndian(long data, ByteArrayOutputStream out) {
        out.write((byte) (data & 0xFF));
        out.write((byte) (data >>> 8));
        out.write((byte) (data >>> 16));
        out.write((byte) (data >>> 24));
        out.write((byte) (data >>> 32));
        out.write((byte) (data >>> 40));
        out.write((byte) (data >>> 48));
        out.write((byte) (data >>> 56));
    }

    public static void writeUnsignedIntLittleEndian(long data, ByteArrayOutputStream out) {
        out.write((byte) (data & 0xFF));
        out.write((byte) (data >>> 8));
        out.write((byte) (data >>> 16));
        out.write((byte) (data >>> 24));
    }

    public static void writeUnsignedInt64LittleEndian(long data, ByteArrayOutputStream out) {
        out.write((byte) (data & 0xFF));
        out.write((byte) (data >>> 8));
        out.write((byte) (data >>> 16));
        out.write((byte) (data >>> 24));
        out.write((byte) (data >>> 32));
        out.write((byte) (data >>> 40));
        out.write((byte) (data >>> 48));
        out.write((byte) (data >>> 56));
    }

    public static void writeUnsignedShortLittleEndian(int data, ByteArrayOutputStream out) {
        out.write((byte) (data & 0xFF));
        out.write((byte) ((data >>> 8) & 0xFF));
    }

    public static void writeUnsignedMediumLittleEndian(int data, ByteArrayOutputStream out) {
        out.write((byte) (data & 0xFF));
        out.write((byte) ((data >>> 8) & 0xFF));
        out.write((byte) ((data >>> 16) & 0xFF));
    }

    /**
     * @param data
     * @param out
     * @throws IOException
     */
    public static void writeBinaryCodedLengthBytes(byte[] data, ByteArrayOutputStream out) throws IOException {
        // 1. write length byte/bytes
        if (data.length < 252) {
            out.write((byte) data.length);
        } else if (data.length < (1 << 16L)) {
            out.write((byte) 252);
            writeUnsignedShortLittleEndian(data.length, out);
        } else if (data.length < (1 << 24L)) {
            out.write((byte) 253);
            writeUnsignedMediumLittleEndian(data.length, out);
        } else {
            out.write((byte) 254);
            writeUnsignedIntLittleEndian(data.length, out);
        }
        // 2. write real data followed length byte/bytes
        out.write(data);
    }

    public static void writeFixedLengthBytes(byte[] data, int index, int length, ByteArrayOutputStream out) {
        for (int i = index; i < index + length; i++) {
            out.write(data[i]);
        }
    }

    public static void writeFixedLengthBytesFromStart(byte[] data, int length, ByteArrayOutputStream out) {
        writeFixedLengthBytes(data, 0, length, out);
    }
    public LengthCodedStringReader getNewLengthCodedStringReader(String encoding ,int index){
        return new LengthCodedStringReader(encoding,index);
    }
    public LengthCodedStringReader getNewLengthCodedStringReaderUTF8(int index){
        return new LengthCodedStringReader(index);
    }
    public LengthCodedStringReader getNewLengthCodedStringReaderUTF8IDX0(){
        return new LengthCodedStringReader();
    }

    public class LengthCodedStringReader {
        public static final String CODE_PAGE_UTF8 = "UTF-8";
        private String encoding;
        private int index = 0;

        public LengthCodedStringReader(String encoding, int startIndex) {
            this.encoding = encoding;
            this.index = startIndex;
        }
        public LengthCodedStringReader(int startIndex){
            encoding=null;
            index=startIndex;
        }
        public LengthCodedStringReader(){
            encoding=null;
            index=0;
        }
        public String readLengthCodedString(byte[] data) throws IOException {
            byte[] lengthBytes = ByteUtil.readBinaryCodedLengthBytes(data, getIndex());
            long length = ByteUtil.readLengthCodedBinary(data, getIndex());
            setIndex(getIndex() + lengthBytes.length);
            if (ByteUtil.NULL_LENGTH == length|ByteUtil.ZERO_LENGTH==length) {
                return null;
            }

            try {
                return new String(ArrayUtils.subarray(data, getIndex(), (int) (getIndex() + length)),
                        encoding == null ? CODE_PAGE_UTF8 : encoding);
            } finally {
                setIndex((int) (getIndex() + length));
            }
        }



        public void setIndex(int index) {
            this.index = index;
        }
        public int getIndex() {
            return index;
        }
    }
}
